/*
 * Segue Audio Player
 * Copyright 2011 Fraser McCrossan frasermccrossan@gmail.com
 * 
 * This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published
 *  by the Free Software Foundation, either version 3 of the License,
 *  or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.

 */

package com.frasermccrossan.segue.providers;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Locale;
import java.util.Random;

import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.preference.PreferenceManager;
import android.provider.BaseColumns;
import android.provider.MediaStore;
import android.provider.MediaStore.Audio.AudioColumns;
import android.util.Log;

public class QueueProvider extends ContentProvider {

    private static final String TAG = "QueueProvider";

    private static final String DATABASE_NAME = "queues.db";

    private static final int DATABASE_VERSION = 1;

    public static final String QUEUE_TABLE = "queued_tracks";

    public static final String AUTHORITY = "frasermccrossan.apps.segue.providers." + TAG;

    public static final String QUEUE_PATH = "queue";
    
    public static final Uri CONTENT_URI = Uri.parse("content://"
			+ QueueProvider.AUTHORITY + "/" + QUEUE_PATH);

	public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.frasermccrossan.queue";

    // in the play order, 0 is the current track, positive is yet to be played,
	// negative is previously played
	public static final Integer CURRENT_TRACK = 0;
	public static final Integer NEXT_TRACK = CURRENT_TRACK + 1;
	private static final Integer PREVIOUS_TRACK_LIMIT = -200;
	
	// magic update strings - these cause large updates when passed to the update() method
	// that are too inefficient to do the normal way
	public static final String MAGIC_RANDOM_SHUFFLE = "magic_random_shuffle"; // shuffle completely randomly
	public static final String MAGIC_ARTIST_SHUFFLE = "magic_artist_shuffle"; // interleaved-artist shuffle
	public static final String MAGIC_NULL = "magic_null"; // null update that just generatesan update notification for any views
	public static final String MAGIC_BACK_UP = "magic_backup"; // backs entire queue up by one (to play previous)
	public static final String MAGIC_PLAY_NEXT = "magic_playnext"; // plays a particular track next
	// magic query strings - these are too inefficient to do the normal way
	public static final String MAGIC_RANDOM_TRACK = "magic_random"; // efficiently fetch a random track
	
	public static final String ID = "_ID";
	public static final String ORDER = "play_order";
	public static final String CP_ID = "cp_id";
	//private static final String QUEUES = "queues";
	//private static final String NAME = "name";
	
    private static final UriMatcher sUriMatcher;

    private static final int QUEUES = 1;

	public class DatabaseHelper extends SQLiteOpenHelper {

		DatabaseHelper(Context context) {
			super(context, DATABASE_NAME, null, DATABASE_VERSION);
		}
		
		@Override
		public void onCreate(SQLiteDatabase db) {
			db.execSQL(String.format(Locale.US, "create table %s (%s integer primary key, %s integer, %s integer, %s text, %s text, %s text, %s text, %s text)",
									 QUEUE_TABLE,
									 BaseColumns._ID,
									 ORDER,
									 CP_ID,
									 AudioColumns.TITLE,
									 AudioColumns.ARTIST,
									 AudioColumns.ALBUM,
									 AudioColumns.TRACK,
									 AudioColumns.DURATION));
//			db.execSQL(String.format(Locale.US, "create table %s (%s integer primary key, %s text)",
//								     QUEUES,
//								     BaseColumns._ID,
//								     NAME));
		}
		
		@Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            Log.w(TAG, "Upgrading database from version " + oldVersion + " to " + newVersion
                    + ", which will destroy all old data");
            db.execSQL("DROP TABLE IF EXISTS " + QUEUE_TABLE);
            onCreate(db);
        }
    }

    private DatabaseHelper dbHelper;

    private void dropExcessivePrevious(SQLiteDatabase db) {
    	db.delete(QUEUE_TABLE, ORDER + "<?", new String[] { PREVIOUS_TRACK_LIMIT.toString() });
    }
    
    private void fixQueueOrder(SQLiteDatabase db) {
    	// get current track _IDs in order
    	Cursor c;
    	// first do non-negative
    	c = db.query(QUEUE_TABLE,
				 new String[] { BaseColumns._ID },
				 ORDER + ">=?",
				 new String[] { CURRENT_TRACK.toString() },
				 null, null, ORDER);
    	if (c.moveToFirst()) {
    		ArrayList<String> ids = new ArrayList<String>(c.getCount());
    		do {
    			ids.add(c.getString(0));
    		} while (c.moveToNext());
    		// now we have all IDs in order, update all of the row order fields
    		// in strict sequence
    		Integer order = CURRENT_TRACK;
    		for (String id : ids) {
    			ContentValues v = new ContentValues(1);
    			v.put(ORDER, order);
    			db.update(QUEUE_TABLE, v, BaseColumns._ID + "=?", new String[] { id });
    			++order;
    		}
    	}
    	c.close();
    	// now negative
    	c = db.query(QUEUE_TABLE,
				 new String[] { BaseColumns._ID },
				 ORDER + "<?",
				 new String[] { CURRENT_TRACK.toString() },
				 null, null, ORDER + " desc");
    	if (c.moveToFirst()) {
    		ArrayList<String> ids = new ArrayList<String>(c.getCount());
    		do {
    			ids.add(c.getString(0));
    		} while (c.moveToNext());
    		// now we have all IDs in order, update all of the row order fields
    		// in strict sequence
    		Integer order = CURRENT_TRACK - 1;
    		for (String id : ids) {
    			ContentValues v = new ContentValues(1);
    			v.put(ORDER, order);
    			db.update(QUEUE_TABLE, v, BaseColumns._ID + "=?", new String[] { id });
    			--order;
    		}
    	}
    	c.close();
    	dropExcessivePrevious(db);
    }
    
    @Override
    // when deleting, move later orders down, unless we are deleting CURRENT_TRACK,
    // in which case just move everything down
    public int delete(Uri uri, String where, String[] whereArgs) {
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        int count;
        switch (sUriMatcher.match(uri)) {
            case QUEUES:
            	Cursor c;
            	Integer orderBeingDeleted = CURRENT_TRACK - 1;
            	c = db.query(QUEUE_TABLE,
        				 new String[] { ORDER },
        				 where,
        				 whereArgs,
        				 null, null, null);
            	if (c.moveToFirst()) {
            		orderBeingDeleted = c.getInt(0);
            		count = 1;
            	}
            	else {
            		count = 0;
            	}
            	c.close();
            	if (orderBeingDeleted == CURRENT_TRACK) {
            		// move everything down by one, moving the current track to
            		// recently played
            		db.beginTransaction();
                	String query = String.format("update %s set %s = %s - 1",
							 QUEUE_TABLE,
							 ORDER, ORDER);
                	db.execSQL(query);
                	query = String.format("delete from %s where %s < ?",
                						  QUEUE_TABLE,
                						  ORDER, PREVIOUS_TRACK_LIMIT);
                	db.execSQL(query);
                	dropExcessivePrevious(db);
                	db.setTransactionSuccessful();
                	db.endTransaction();
            	}
            	else {
            		// delete and fix everything else
            		db.beginTransaction();
            		count = db.delete(QUEUE_TABLE, where, whereArgs);
            		fixQueueOrder(db);
            		db.setTransactionSuccessful();
            		db.endTransaction();
            	}
                break;

            default:
                throw new IllegalArgumentException("Unknown URI " + uri);
        }
        getContext().getContentResolver().notifyChange(uri, null);
        return count;
    }

    @Override
    public String getType(Uri uri) {
        switch (sUriMatcher.match(uri)) {
            case QUEUES:
                return CONTENT_TYPE;

            default:
                throw new IllegalArgumentException("Unknown URI " + uri);
        }
    }

    @Override
    // this implementation automatically updates all ORDER fields if an ORDER field
    // is passed in iv
    public Uri insert(Uri uri, ContentValues iv) {
        if (sUriMatcher.match(uri) != QUEUES) { throw new IllegalArgumentException("Unknown URI " + uri); }

        ContentValues values;
        if (iv != null) {
            values = new ContentValues(iv);
        } else {
            values = new ContentValues();
        }

        SQLiteDatabase db = dbHelper.getWritableDatabase();
        if (iv.containsKey(ORDER)) {
        	// shuffle all orders >= the new one down by one
        	String query = String.format("update %s set %s = %s + 1 where %s >= %d",
        								 QUEUE_TABLE,
        								 ORDER, ORDER,
        								 ORDER, iv.getAsInteger(ORDER));
        	db.execSQL(query);
        }
        long rowId = db.insert(QUEUE_TABLE, null, values);
        if (rowId > 0) {
            Uri noteUri = ContentUris.withAppendedId(CONTENT_URI, rowId);
            getContext().getContentResolver().notifyChange(uri, null);
            return noteUri;
        }

        throw new SQLException("Failed to insert row into " + uri);
    }

    @Override
    public boolean onCreate() {
        dbHelper = new DatabaseHelper(getContext());
        return true;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
    	Cursor c;
        switch (sUriMatcher.match(uri)) {
            case QUEUES:
                SQLiteDatabase db = dbHelper.getReadableDatabase();
                if (selection == MAGIC_RANDOM_TRACK) {
                	c = db.query(QUEUE_TABLE, projection, null, null, null, null, "random()", "1");
                }
                else {
                	c = db.query(QUEUE_TABLE, projection, selection, selectionArgs, null, null, sortOrder);
                }
                c.setNotificationUri(getContext().getContentResolver(), uri);
                break;
            default:
                throw new IllegalArgumentException("Unknown URI " + uri);
        }

        c.setNotificationUri(getContext().getContentResolver(), uri);
        return c;
    }

    @Override
    public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        int count = 0;
        switch (sUriMatcher.match(uri)) {
            case QUEUES:
            	if (where == MAGIC_RANDOM_SHUFFLE) {
            		shuffle_random(db);
            	}
            	else if (where == MAGIC_ARTIST_SHUFFLE) {
            		shuffle_interleave(db);
            	}
            	else if (where == MAGIC_BACK_UP) {
            		db.execSQL(String.format("update %s set %s = %s + 1",
            				QUEUE_TABLE, ORDER, ORDER));
            	}
            	else if (where == MAGIC_PLAY_NEXT) {
            		// the track order to move to next position is expected in whereArgs[0]
            		Cursor c = db.query(QUEUE_TABLE, new String[] { MediaStore.Audio.Media._ID }, ORDER + "=?", whereArgs, null, null, null);
            		if (c != null) {
            			if (c.moveToFirst()) {
            				int toPlayNextId = c.getInt(0);
                    		db.beginTransaction();
                    		db.execSQL(String.format("update %s set %s = %s + 1 where %s >= %d",
                    				QUEUE_TABLE, ORDER, ORDER, ORDER, NEXT_TRACK));
                    		db.execSQL(String.format("update %s set %s = %d where %s = %d",
                    				QUEUE_TABLE, ORDER, NEXT_TRACK, MediaStore.Audio.Media._ID, toPlayNextId));
                    		fixQueueOrder(db);
                    		db.setTransactionSuccessful();
                    		db.endTransaction();
            			}
            			c.close();
            		}
            	}
            	else if (where == MAGIC_NULL) {
            		// do nothing!
            	}
            	else {
            		count = db.update(QUEUE_TABLE, values, where, whereArgs);
            		if (values.containsKey(ORDER)) {
            			db.beginTransaction();
            			fixQueueOrder(db); // if we have updated any ORDER fields, reset them
            			db.setTransactionSuccessful();
            			db.endTransaction();
            		}
            	}
                break;

            default:
                throw new IllegalArgumentException("Unknown URI " + uri);
        }

        getContext().getContentResolver().notifyChange(uri, null);
        return count;
    }

    // a completely random shuffle, with possibility of non-uniform distribution
    public void shuffle_random(SQLiteDatabase db) {
		db.beginTransaction();
		db.execSQL(String.format("update %s set %s = abs(random()/2) + 1 where %s > %d",
				                 QUEUE_TABLE, ORDER, ORDER, CURRENT_TRACK));
		fixQueueOrder(db);
		db.setTransactionSuccessful();
		db.endTransaction(); 	
    }
    
    // an interleaved shuffle that guarantees that different artists will not be adjacent
    public void shuffle_interleave(SQLiteDatabase db) {
    	HashMap<String, ArrayList<String>> tl_by_artist= new HashMap<String, ArrayList<String>>();
    	
    	Cursor c = db.query(QUEUE_TABLE, new String[] { ID, MediaStore.Audio.Media.ARTIST },
    				        ORDER + ">?",
    				        new String[] { CURRENT_TRACK.toString() },
    				        null, null, null);
    	if (c != null) {
    		if (c.moveToFirst()) {
    			do {
    				String id = c.getString(0);
    				String artist = c.getString(1);
    				if (! tl_by_artist.containsKey(artist)) {
    					tl_by_artist.put(artist, new ArrayList<String>(1));
    				}
    				tl_by_artist.get(artist).add(id);
    			} while (c.moveToNext());
    		}
    		c.close();
    		// now we have a hash which, for each key, contains a list of row IDs for records of each artist
    		// get a randomized list of artists
    		ArrayList<String> artists = new ArrayList<String>(tl_by_artist.keySet());
    		randomizeList(artists);
    		// and randomize each track list
    		for (String artist : artists) {
    			randomizeList(tl_by_artist.get(artist));
    		}
    		ArrayList<String> interleaved = new ArrayList<String>();
    		int copied;
    		do {
    			copied = 0; // number copied this iteration - yeah, inefficient but simple
    			for (String artist : artists) {
    				ArrayList<String> trackList = tl_by_artist.get(artist);
    				if (trackList.size() > 0) {
    					interleaved.add(trackList.get(0));
    					trackList.remove(0);
    					++copied;
    				}
    			}
    		} while (copied > 0);
    		db.beginTransaction();
    		int i;
    		for (i = 0; i < interleaved.size(); ++i) {
    			ContentValues cv = new ContentValues(1);
    			cv.put(ORDER, i + 1); // list indexed from 0, table ORDER goes from 1 (0 is current track)
    			db.update(QUEUE_TABLE, cv, ID + "=?", new String[] { interleaved.get(i) });
    		}
    		db.setTransactionSuccessful();
    		db.endTransaction();
    	}
    }
    
    public void randomizeList(ArrayList<String> list) {
    	int size = list.size();
    	String temp; // for exchanges
    	int i, j;
    	Random r = new Random();
    	for (i = size - 1; i >= 1; --i) {
    		j = r.nextInt(i+1);
    		if (i != j) { // only need to exchange for different indices
    			temp = list.get(i);
    			list.set(i, list.get(j));
    			list.set(j, temp);
    		}
    	}
    }
    
    static {
        sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        sUriMatcher.addURI(AUTHORITY, QUEUE_PATH, QUEUES);

    }
}
